import { Injectable } from '@angular/core';
import { Events } from 'ionic-angular';
import { BehaviorSubject } from "rxjs/Rx";
import { TextEncoder, TextDecoder } from 'text-encoding';

declare var chrome;

@Injectable()
export class UdpService {
  private extraHeadLen = 5;
  private extraLen = 40;
  private _datagramCount = 0;
  private wifiConfigDone = false;
  private isWaitingForResponse;

  private mEsptouchResultOneLen = 1;
  private mEsptouchResultMacLen = 6;
  private mEsptouchResultIpLen = 4;
  private mEsptouchResultTotalLen = 1 + 6 + 4;

  private udpstream: BehaviorSubject<Object>;

  // 256 max len byte array
  crcTable = [];

  private mPortListening = 18266;
  private mTargetPort = 7001;

  constructor(private events: Events) {
    const polynomial = 0x8c;

    for (let dividend = 0; dividend < 256; dividend++) {
      let remainder = dividend;
      for (let bit = 0; bit < 8; bit++) {
        if ((remainder & 0x01) !== 0) {
          remainder = (remainder >>> 1) ^ polynomial;
        } else {
          remainder >>>= 1;
        }
      }

      this.crcTable.push(remainder);
    }
  };

  /**
   * one indivisible data contain 3 9bits info
   */
  getOneDataLen() {
    return 3;
  }

  getPortListening() {
    return this.mPortListening;
  }

  getTargetPort() {
    return this.mTargetPort;
  }

  /**
   * Must check if system supports this plugin before using it!!!!
   */
  isUdpSupported() {
    return typeof chrome.sockets !== 'undefined';
  }

  registerListener() {
    this.wifiConfigDone = false;
    if (this.udpstream) {
      this.udpstream.unsubscribe();
    }

    this.udpstream = new BehaviorSubject({});

    // register the listeners
    chrome.sockets.udp.onReceive.addListener((info) => {
      if (info.data['byteLength'] !== this.mEsptouchResultTotalLen) {
        console.info('Incorrect response, just ignore');
      } else {
        if (!this.wifiConfigDone) {
          this.wifiConfigDone = true;
          this.udpstream.next(info.data);
        }
      }
    });

    chrome.sockets.udp.onReceiveError.addListener((error) => {
      console.log('Recv ERROR from socket: ', error);
      this.udpstream.next({'error': error});
    });

    // return the stream
    return this.udpstream.asObservable().skip(1);
  }

  /**
   * Send a single package and wait for 15 seconds to receive response packets if any.
   * - sourcePort is used to receive packets (local port)
   * - targetAddress:targetPort are used to define remote desitnation when sending packets
   */
  sendData(message: string, sourcePort:number, targetAddress: string, targetPort: number) {
    return this.sendUDPMessage(message, sourcePort, [targetAddress], targetPort);
  }

  private sendUDPMessage(message: string, sourcePort: number, addresses: Array<string>, targetPort: number) {
    // translate the string into ArrayBuffer
    const arr = this.stringToByte(message);
    let buf = new ArrayBuffer(arr.length);
    const bufView = new Int8Array(buf);
    for (var i = 0; i < arr.length; i++) {
      bufView[i] = arr[i];
    }

    // send  the UDP search as captures in UPNPSTRING and to port PORT
    chrome.sockets.udp.create((createInfo) => {
      console.info('Created: sockiet id=' + createInfo.socketId);
      chrome.sockets.udp.bind(createInfo.socketId, '0.0.0.0', sourcePort, (bindresult) => {
        console.info('Bind: sockiet id=' + createInfo.socketId + ', result=' + bindresult);
        chrome.sockets.udp.setBroadcast(createInfo.socketId, true, function (sbresult) {
          console.info('SetBroadcast: sockiet id=' + createInfo.socketId + ', result=' + sbresult);
          // do all adresses
          addresses.map(address => {
            chrome.sockets.udp.send(createInfo.socketId, buf, address, targetPort, (sendresult) => {
              if (sendresult.resultCode < 0) {
                console.log('send fail: ' + sendresult);
                this.udpstream.next({'error': sendresult});
              } else {
                console.log('sendTo: success ' + sourcePort, targetPort, sendresult.bytesSent);
              }
            });
          });
        });
      });
    });
  }

  prepareSocket(succ, fail) {
    chrome.sockets.udp.create((createInfo) => {
      const socketId = createInfo.socketId;
      console.info('Socket ' + socketId + ' was just created.');
      chrome.sockets.udp.bind(socketId, '0.0.0.0', this.getPortListening(), (bindresult) => {
        if (bindresult != 0) {
          if (fail) {
            fail(socketId, 'Failed to bind ip/port.');
          }
          return;
        }

        chrome.sockets.udp.setBroadcast(socketId, true, (sbresult) => {
          if (sbresult != 0) {
            if (fail) {
              fail(socketId, 'Failed to set broadcast.');
            }
            return;
          } else {
            if (succ) {
              succ(socketId);
            }
          }
        });
      });
    });
  }

  sendBytesDataOnce(socketId, data, index) {
    const targetHost = this.getTargetHostname();
    const targetPort = this.getTargetPort();
    chrome.sockets.udp.send(socketId, data[index], targetHost, targetPort, (sendresult) => {
      if (sendresult.resultCode < 0) {
        console.log('send fail: ' + sendresult);
        this.udpstream.next({'error': sendresult});
      }
    });
  }

  // broadcast GC data firstly with 300 times
  broadcastData(socketId, gcData, dcData) {
    const max = 300;
    const count = gcData.length;

    let total = 0;
    let index = 0;
    const gcJob = setInterval(() => {
      this.sendBytesDataOnce(socketId, gcData, index);
      index = (index + 1) % count;
      total++;
      if (total > max) {
        clearInterval(gcJob);
        this.broadcastDcData(socketId, dcData)
      }
    }, 5);
  }

  // broadcast 800 times
  broadcastDcData(socketId, dcData) {
    const max = 800;
    const count = dcData.length;

    let total = 0;
    let index = 0;
    const dcJob = setInterval(() => {
      this.sendBytesDataOnce(socketId, dcData, index);
      index = (index + 1) % count;
      total++;
      if (total > max) {
        clearInterval(dcJob);
        this.closeUDPService(socketId);
        // check if the job is done
        if (!this.wifiConfigDone) {
          // close the stream after 5 seconds
          this.isWaitingForResponse = setTimeout(() => {
            this.stopReceiving();

            // usually, this means failure!
            if (!this.wifiConfigDone) {
              this.wifiConfigDone = true;
              this.events.publish('wifi:failed');
            }
          }, 5000);
        }
      }
    }, 8);
  }

  closeUDPService(socketId) {
    chrome.sockets.udp.close(socketId);
  }

  stopReceiving() {
    if (this.udpstream) {
      this.udpstream.complete();
      this.udpstream.unsubscribe();
    }

    if (this.isWaitingForResponse) {
      clearTimeout(this.isWaitingForResponse);
    }
  }

  /**
   * return Int8Array
   */
  stringToByte(str) {
    return new Int8Array(new TextEncoder().encode(str));
  }

  byteToString(arr) {
    return new TextDecoder().decode(arr);
  }

  checksum(start, buff) {
    let value = start ? start : 0;

    if (buff.constructor !== Int8Array) {
      buff = Int8Array.from([buff]);
    }

    for (let i = 0; i < buff.length; i++) {
      value = this.crcTable[(value ^ (buff[i] & 0xff)) %256];
    }

    return value;
  }

  /**
   * Split uint8 to 2 bytes of high byte and low byte. e.g. 20 = 0x14 should
   * be split to [0x01,0x04] 0x01 is high byte and 0x04 is low byte
   */
  private splitUint8To2bytes(uint8) {
    const hexString = uint8.toString(16);
    const arr = new Uint8Array(2);
    if (hexString.length > 1) {
      arr[0] = parseInt(hexString.substring(0, 1), 16);
      arr[1] = parseInt(hexString.substring(1, 2), 16);
    } else {
      arr[0] = 0;
      arr[1] = parseInt(hexString.substring(0, 1), 16);
    }

    return arr;
  }

  private combine2bytesToOne(uint8Arr) {
    return (uint8Arr[0] << 4) | uint8Arr[1];
  }

  /**
   * index must <= 127.
   */
  getDataCodeBytes(uint8, index) {
    const uint8Arr = this.splitUint8To2bytes(uint8);
    const mUint8High = uint8Arr[0];
    const mUint8Low = uint8Arr[1];

    let crc8 = this.checksum(0, uint8);
    crc8 = this.checksum(crc8, index);
    const crc8Arr = this.splitUint8To2bytes(crc8);
    const mCrc8High = crc8Arr[0];
    const mCrc8Low = crc8Arr[1];

    const arr = new Uint8Array(6);
    arr[0] = 0x00;
    arr[1] = this.combine2bytesToOne(Int8Array.from([mCrc8High, mUint8High]));
    arr[2] = 0x01;
    arr[3] = index;
    arr[4] = 0x00;
    arr[5] = this.combine2bytesToOne(Int8Array.from([mCrc8Low, mUint8Low]));

    return arr;
  }

  private getDatumCode(apSsid, apBssid, apPassword, ipAddress, isSsidHiden) {
    let totalXor = 0;

    const apPwdLen = apPassword.length;

    const apSsidCrc = this.checksum(0, apSsid);

    const apBssidCrc = this.checksum(0, apBssid);

    const apSsidLen = apSsid.length;

    // IP parse
    const ipAddrStrs = ipAddress.split(".");
    const ipLen = ipAddrStrs.length;

    const ipAddrChars = [];
    ipAddrStrs.forEach(ipSeg => {
      ipAddrChars.push(parseInt(ipSeg));
    });

    const _totalLen = this.extraHeadLen + ipLen + apPwdLen + apSsidLen;
    const totalLen = isSsidHiden ? (this.extraHeadLen + ipLen + apPwdLen + apSsidLen) : (this.extraHeadLen + ipLen + apPwdLen);

    const baBuffer = [];
    baBuffer.push(this.getDataCodeBytes(_totalLen, 0));
    totalXor ^= _totalLen;

    baBuffer.push(this.getDataCodeBytes(apPwdLen, 1));
    totalXor ^= apPwdLen;

    baBuffer.push(this.getDataCodeBytes(apSsidCrc, 2));
    totalXor ^= apSsidCrc;

    baBuffer.push(this.getDataCodeBytes(apBssidCrc, 3));
    totalXor ^= apBssidCrc;

    // ESPDataCode 4 is null, will be inserted later on

    for (let i = 0; i < ipLen; ++i) {
      baBuffer.push(this.getDataCodeBytes(ipAddrChars[i], i + this.extraHeadLen));
      totalXor ^= ipAddrChars[i];
    }

    const apPwdBytes = apPassword;
    const apPwdChars = [];
    for (let i = 0; i < apPwdBytes.length; i++) {
      apPwdChars.push(this.convertByte2Uint8(apPwdBytes[i]));
    }

    for (let i = 0; i < apPwdChars.length; i++) {
      baBuffer.push(this.getDataCodeBytes(apPwdChars[i], i + this.extraHeadLen + ipLen));
      totalXor ^= apPwdChars[i];
    }

    const apSsidBytes = apSsid;
    const apSsidChars = [];
    // totalXor will xor apSsidChars no matter whether the ssid is hidden
    for (let i = 0; i < apSsidBytes.length; i++) {
      apSsidChars[i] = this.convertByte2Uint8(apSsidBytes[i]);
      totalXor ^= apSsidChars[i];
    }

    if (isSsidHiden) {
      for (let i = 0; i < apSsidChars.length; i++) {
        baBuffer.push(this.getDataCodeBytes(apSsidChars[i], i + this.extraHeadLen + ipLen + apPwdLen));
      }
    }

    // add total xor last at position 4
    baBuffer.splice(4, 0, this.getDataCodeBytes(totalXor, 4));

    // add bssid
    let bssidInsertIndex = this.extraHeadLen;
    for (let i = 0; i < apBssid.length; i++) {
      let index = totalLen + i;
      const c = this.convertByte2Uint8(apBssid[i]);
      const dc = this.getDataCodeBytes(c, index);
      if (bssidInsertIndex >= baBuffer.length) {
        baBuffer.push(dc);
      } else {
        baBuffer.splice(bssidInsertIndex, 0, dc);
      }

      bssidInsertIndex += 4;
    }

    // console.info('Got baBuffer with size of ' + baBuffer.length);
    const bytes = new Int8Array(baBuffer.length * 6);
    let index = 0;
    baBuffer.forEach(dc => {
      dc.forEach(byte => {
        bytes[index++] = byte;
      });
    });

    const len = bytes.length / 2;
    const u16 = new Uint16Array(len);
    for (let i = 0; i < len; i++) {
      const high = bytes[i * 2];
      const low = bytes[i * 2 + 1];
      u16[i] = this.combine2bytesToU16(high, low) + this.extraLen;
    }

    return u16;
  }

  private convertByte2Uint8(input) {
    // & 0xff could make negatvie value to positive
    return input & 0xff;
  }

  private combine2bytesToU16(high, low) {
    return this.convertByte2Uint8(high) << 8 | this.convertByte2Uint8(low);
  }

  /**
   * Guide code are: 515, 514, 513, 512
   *
   * Returns an array with 4 ArrayBuffer objects, each length represents the guide code.
   */
  genGuideCode() {
    const guides = [515, 514, 513, 512];

    // fixed byte value with char '1'
    const code = new TextEncoder().encode('1')[0];

    const guideData = [];
    guides.forEach(guide => {
      const buf = new ArrayBuffer(guide);
      const bufView = new Uint8Array(buf);
      for (var i = 0; i < guide; i++) {
        bufView[i] = code;
      }

      guideData.push(buf);
    });

    return guideData;
  }

  /**
   * Inputs as below:
   * - ssid as string like ryli
   * - bssid as string like aa:bb:cc:ee
   * - password as string like 123456
   * - ipAddress like 123.1.2.3
   * - isSsiHiden as boolen like false
   *
   * Returns an array with 4 ArrayBuffer objects, each length represents the datum code.
   */
  genDatumCode(ssid, bssid, password, ipAddress, isSsidHiden) {
    const apSsid = this.stringToByte(ssid);
    const apBssid = this.parseBssid2bytes(bssid);
    const apPassword = this.stringToByte(password);
    const datums = this.getDatumCode(apSsid, apBssid, apPassword, ipAddress, isSsidHiden);

    // fixed byte value with char '1'
    const code = new TextEncoder().encode('1')[0];

    const datumData = [];
    datums.forEach(datum => {
      const buf = new ArrayBuffer(datum);
      const bufView = new Uint8Array(buf);
      for (var i = 0; i < datum; i++) {
        bufView[i] = code;
      }

      // console.info('length: ' + buf.byteLength);
      datumData.push(buf);
    });

    return datumData;
  }

  /**
   * parse bssid - The bssid like aa:bb:cc:dd:ee:ff
   */
  parseBssid2bytes(bssid: string) {
    const bssidSplits = bssid.split(":");
    const bytes = new Int8Array(bssidSplits.length);

    for (let i = 0; i < bssidSplits.length; i++) {
      bytes[i] = parseInt(bssidSplits[i], 16);
    }

    return bytes;
  }

  /**
   * For each round of work, need to reset this counter!
   */
  resetDatagramCount() {
    this._datagramCount = 0;
  }

  getTargetHostname() {
    // return '192.168.1.255';
    const count = 1 + (this._datagramCount++) % 100;
    return "234." + count + "." + count + "." + count;
  }

  parseBssid(buffer) {
    const int8Array = new Uint8Array(buffer, this.mEsptouchResultOneLen, this.mEsptouchResultMacLen);
    const bssids = [];
    int8Array.forEach(item => {
      bssids.push(item.toString(16));
    });
    return bssids.join(':');
  }

  parseInetAddr(buffer) {
    const int8Array = new Uint8Array(buffer, this.mEsptouchResultOneLen + this.mEsptouchResultMacLen, this.mEsptouchResultIpLen);
    return int8Array.join('.');
  }
}
